ImGui中Vulkan的窗口渲染

先看看ImGui中的Vulkan窗口结构体:

struct ImGui_ImplVulkanH_Window
{
    int                 Width;
    int                 Height;
    VkSwapchainKHR      Swapchain;
    VkSurfaceKHR        Surface;
    VkSurfaceFormatKHR  SurfaceFormat;
    VkPresentModeKHR    PresentMode;
    VkRenderPass        RenderPass;
    VkPipeline          Pipeline;               // The window pipeline may uses a different VkRenderPass than the one passed in ImGui_ImplVulkan_InitInfo
    bool                ClearEnable;
    VkClearValue        ClearValue;
    uint32_t            FrameIndex;             // Current frame being rendered to (0 <= FrameIndex < FrameInFlightCount)
    uint32_t            ImageCount;             // Number of simultaneous in-flight frames (returned by vkGetSwapchainImagesKHR, usually derived from min_image_count)
    uint32_t            SemaphoreIndex;         // Current set of swapchain wait semaphores we're using (needs to be distinct from per frame data)
    ImGui_ImplVulkanH_Frame*            Frames;
    ImGui_ImplVulkanH_FrameSemaphores*  FrameSemaphores;

    ImGui_ImplVulkanH_Window()
    {
        memset((void*)this, 0, sizeof(*this));
        PresentMode = (VkPresentModeKHR)~0;     // Ensure we get an error if user doesn't set this.
        ClearEnable = true;
    }
};

后文所做的事情无非就是把这个结构体填充完毕。

Setup Vulkan

首先先声明一下后面会使用的变量

static VkAllocationCallbacks* g_Allocator = NULL;
static VkInstance               g_Instance = VK_NULL_HANDLE;
static VkPhysicalDevice         g_PhysicalDevice = VK_NULL_HANDLE;
static VkDevice                 g_Device = VK_NULL_HANDLE;
static uint32_t                 g_QueueFamily = (uint32_t)-1;
static VkQueue                  g_Queue = VK_NULL_HANDLE;
static VkDebugReportCallbackEXT g_DebugReport = VK_NULL_HANDLE;
static VkPipelineCache          g_PipelineCache = VK_NULL_HANDLE;
static VkDescriptorPool         g_DescriptorPool = VK_NULL_HANDLE;

static ImGui_ImplVulkanH_Window g_MainWindowData;// 这是imgui用来控制vulkan渲染到窗口的
static int                      g_MinImageCount = 2;
static bool                     g_SwapChainRebuild = false;

然后按顺序执行下列步骤。

创建Vulkan Instance

官方教程说Vulkan Instance维护了每个Vulkan应用的状态,因此必须先进行vulkan instance的创建。此文章也说明了vulkan instance的初始化过程中会加载并初始化GPU驱动。

VkInstanceCreateInfo create_info = {};
create_info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
create_info.enabledExtensionCount = extensions_count;
create_info.ppEnabledExtensionNames = extensions;
err = vkCreateInstance(&create_info, g_Allocator, &g_Instance);

在退出应用时需要自行销毁此instance。

获取GPU设备

下列代码只是获取了GPU设备信息之后选择了一个GPU设备作为此引用的vulkan设备,并把设备信息储存起来,没有进行其他操作。

uint32_t gpu_count;
err = vkEnumeratePhysicalDevices(g_Instance, &gpu_count, NULL);
check_vk_result(err);
IM_ASSERT(gpu_count > 0);

VkPhysicalDevice* gpus = (VkPhysicalDevice*)malloc(sizeof(VkPhysicalDevice) * gpu_count);
err = vkEnumeratePhysicalDevices(g_Instance, &gpu_count, gpus);
check_vk_result(err);

// If a number >1 of GPUs got reported, find discrete GPU if present, or use first one available. This covers
// most common cases (multi-gpu/integrated+dedicated graphics). Handling more complicated setups (multiple
// dedicated GPUs) is out of scope of this sample.
// 这里优先选择独显,如无独显则选择获取的第一个GPU设备
int use_gpu = 0;
for (int i = 0; i < (int)gpu_count; i++)
{
    VkPhysicalDeviceProperties properties;
    vkGetPhysicalDeviceProperties(gpus[i], &properties);
    if (properties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU)
    {
        use_gpu = i;
        break;
    }
}

g_PhysicalDevice = gpus[use_gpu];
free(gpus);

选择队列族

[Vulkan 的几乎所有操作,从绘制到加载纹理都需要将操作 指令提交给一个队列,然后才能执行。Vulkan 有多种不同类型的队列,它们属于不同的队列族,每个队列族的队列只允许执行特定的一部分指令。 比如,可能存在只允许执行计算相关指令的队列族和只允许执行内存传输的队列族。](Vulkan从入门到精通31-队列族和逻辑设备 - 知乎 (zhihu.com))

uint32_t count;
vkGetPhysicalDeviceQueueFamilyProperties(g_PhysicalDevice, &count, NULL);
VkQueueFamilyProperties* queues = (VkQueueFamilyProperties*)malloc(sizeof(VkQueueFamilyProperties) * count);
vkGetPhysicalDeviceQueueFamilyProperties(g_PhysicalDevice, &count, queues);
for (uint32_t i = 0; i < count; i++)
    if (queues[i].queueFlags & VK_QUEUE_GRAPHICS_BIT)
    {
        g_QueueFamily = i;
        break;
    }
free(queues);
IM_ASSERT(g_QueueFamily != (uint32_t)-1);

创建逻辑设备

创建逻辑设备时需要传入queue的参数,因此需要先创建queue info才能创建设备。在创建设备时也会创建queue。 逻辑设备肯定就区别于物理设备,之后需要vulkan进行交互的都是逻辑设备。

int device_extension_count = 1;
const char* device_extensions[] = { "VK_KHR_swapchain" };
const float queue_priority[] = { 1.0f };
VkDeviceQueueCreateInfo queue_info[1] = {};
queue_info[0].sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
queue_info[0].queueFamilyIndex = g_QueueFamily;
queue_info[0].queueCount = 1;
queue_info[0].pQueuePriorities = queue_priority;
VkDeviceCreateInfo create_info = {};
create_info.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
create_info.queueCreateInfoCount = sizeof(queue_info) / sizeof(queue_info[0]);
create_info.pQueueCreateInfos = queue_info;
create_info.enabledExtensionCount = device_extension_count;
create_info.ppEnabledExtensionNames = device_extensions;
err = vkCreateDevice(g_PhysicalDevice, &create_info, g_Allocator, &g_Device);
check_vk_result(err);
vkGetDeviceQueue(g_Device, g_QueueFamily, 0, &g_Queue);

同样地,在程序退出时需要调用kDestroyDevice。

创建描述符池

在vulkan中,descriptor大概是着色器使用的变量,最终组成discriptor set之后才能被shader使用。这篇文章对descriptor解释得蛮清楚。

v2-61ccfca7561dc80ff9e2cebfeaa446e3_1440w

VkDescriptorPoolSize pool_sizes[] =
{
    { VK_DESCRIPTOR_TYPE_SAMPLER, 1000 },
    { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1000 },
    { VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1000 },
    { VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1000 },
    { VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER, 1000 },
    { VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER, 1000 },
    { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1000 },
    { VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1000 },
    { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 1000 },
    { VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC, 1000 },
    { VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT, 1000 }
};
VkDescriptorPoolCreateInfo pool_info = {};
pool_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
pool_info.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
pool_info.maxSets = 1000 * IM_ARRAYSIZE(pool_sizes);
pool_info.poolSizeCount = (uint32_t)IM_ARRAYSIZE(pool_sizes);
pool_info.pPoolSizes = pool_sizes;
err = vkCreateDescriptorPool(g_Device, &pool_info, g_Allocator, &g_DescriptorPool);
check_vk_result(err);

创建SwapChain

首先要使用某些方法创建窗口。

以GLFW为例,可以先用其创建一个窗口。

if (!glfwInit())
    return 1;

glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
GLFWwindow* window = glfwCreateWindow(1920, 1080, "Doxel", NULL, NULL);

接下来就是vulkan时间。

预先准备

创建surface

vulkan中有WSI(Window Surface Intergration)的概念,也就是将vulkan渲染的结果显示在某个window上。

surface的创建方法如下:

VkSurfaceKHR surface;
glfwCreateWindowSurface(g_Instance, window, g_Allocator, &surface);

创建色彩空间和图像(像素)格式

而后创建色彩空间和格式信息。下面的imgui函数封装了访问和匹配vulkan设备支持的格式信息,返回的是VkSurfaceFormatKHR类型值,此类型由色彩空间和格式组成。

const VkFormat requestSurfaceImageFormat[] = { VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_B8G8R8_UNORM, VK_FORMAT_R8G8B8_UNORM };
const VkColorSpaceKHR requestSurfaceColorSpace = VK_COLORSPACE_SRGB_NONLINEAR_KHR;
wd->SurfaceFormat = ImGui_ImplVulkanH_SelectSurfaceFormat(g_PhysicalDevice, wd->Surface, requestSurfaceImageFormat, (size_t)IM_ARRAYSIZE(requestSurfaceImageFormat), requestSurfaceColorSpace);

创建呈现模式信息

呈现模式主要控制了,在渲染出一个图像后,采用什么策略显示到屏幕。各个策略如下,fifo策略在帧数拉满时没有画面撕裂。

  • VK_PRESENT_MODE_IMMEDIATE_KHR:你的程序提交的图像会立即传输到屏幕,这可能会导致撕裂;

  • VK_PRESENT_MODE_FIFO_KHR:交换链是个队列,显示的时候从队列头拿一个图像,程序插入渲染的图像到队列尾。如果队列满了程序就要等待,这差不多像是垂直同步,显示刷新的时刻就是垂直空白;

  • VK_PRESENT_MODE_FIFO_RELAXED_KHR:在最后一个垂直空白的时候,如果应用迟到,且队列为空,该模式才会和前面的那个有所不同。这样就不等到下一个垂直空白,图像会直接传输到屏幕,可能导致撕裂;

  • VK_PRESENT_MODE_MAILBOX_KHR:这是第二个模式的又一个变种,当队列满的时候,它不会阻塞应用,已经在队列中的图像会被新的替换。这个模式可以实现三重缓冲,避免撕裂,比使用双重缓冲的垂直同步减少很多延迟。

很显然,下列代码中的imgui函数也是封装了访问和匹配vulkan设备显示方法。

#ifdef IMGUI_UNLIMITED_FRAME_RATE
    VkPresentModeKHR present_modes[] = { VK_PRESENT_MODE_MAILBOX_KHR, VK_PRESENT_MODE_IMMEDIATE_KHR, VK_PRESENT_MODE_FIFO_KHR };
#else
    VkPresentModeKHR present_modes[] = { VK_PRESENT_MODE_FIFO_KHR };
#endif
    wd->PresentMode = ImGui_ImplVulkanH_SelectPresentMode(g_PhysicalDevice, wd->Surface, &present_modes[0], IM_ARRAYSIZE(present_modes));

创建SwapChain

首先初始化CreateInfo结构体。而后进行SwapChain的创建以及对应Image缓冲的创建。

VkSwapchainCreateInfoKHR info = {};
info.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
info.surface = wd->Surface;
info.minImageCount = min_image_count;
info.imageFormat = wd->SurfaceFormat.format;
info.imageColorSpace = wd->SurfaceFormat.colorSpace;
info.imageArrayLayers = 1;
info.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
info.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;           // Assume that graphics family == present family
info.preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
info.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
info.presentMode = wd->PresentMode;
info.clipped = VK_TRUE;
info.oldSwapchain = old_swapchain;
VkSurfaceCapabilitiesKHR cap; // 这个capability包含了swapchain的最小缓存数和最大缓冲数之类
err = vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physical_device, wd->Surface, &cap);

err = vkCreateSwapchainKHR(device, &info, allocator, &wd->Swapchain);
err = vkGetSwapchainImagesKHR(device, wd->Swapchain, &wd->ImageCount, nullptr);

在imgui里封装了FrameBuffer及其对应的Image和ImageView及需要属性,称作Frame,用来控制buffer和同步的。

struct ImGui_ImplVulkanH_Frame
{
    VkCommandPool       CommandPool;
    VkCommandBuffer     CommandBuffer;
    VkFence             Fence;
    VkImage             Backbuffer;
    VkImageView         BackbufferView;
    VkFramebuffer       Framebuffer;
};

struct ImGui_ImplVulkanH_FrameSemaphores
{
    VkSemaphore         ImageAcquiredSemaphore;
    VkSemaphore         RenderCompleteSemaphore;
};

创建Render Pass

vulkan中的Render Pass和Unity中Shader的Render Pass感觉上来说不同,拿延迟渲染举例,延迟渲染有渲染GBuffer的阶段和屏幕空间计算光照的部分(我也不知道为什么这么分成两个阶段),整个延迟渲染是一个Render Pass,但分为了GBuffer和屏幕空间两个pipeline,所以Render Pass应该是最高层的渲染流程。[引用]

// attachment
VkAttachmentDescription attachment = {};
attachment.format = wd->SurfaceFormat.format;
attachment.samples = VK_SAMPLE_COUNT_1_BIT;
attachment.loadOp = wd->ClearEnable ? VK_ATTACHMENT_LOAD_OP_CLEAR : VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
attachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
attachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
// attachment reference
VkAttachmentReference color_attachment = {};
color_attachment.attachment = 0;
color_attachment.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
// 一个Render Pass由若干Subpass组成
VkSubpassDescription subpass = {};
// bind point有compute和graphics两种,compute是计算管线,graphics是图形渲染管线
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &color_attachment;
// subpass依赖
VkSubpassDependency dependency = {};
// VK_SUBPASS_EXTERNAL表示了该subpass之前的subpass
dependency.srcSubpass = VK_SUBPASS_EXTERNAL;
// 0表示当前subpass吧大概
dependency.dstSubpass = 0;
dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dependency.srcAccessMask = 0;
dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
// Render Pass Create Info
VkRenderPassCreateInfo info = {};
info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
info.attachmentCount = 1;
info.pAttachments = &attachment;
info.subpassCount = 1;
info.pSubpasses = &subpass;
info.dependencyCount = 1;
info.pDependencies = &dependency;
err = vkCreateRenderPass(device, &info, allocator, &wd->RenderPass);
check_vk_result(err);

创建ImageView

管线着色器不能直接访问图形对象。作为替代,image view相当于一个代理,代表了image所占据的连续内存区域,并且包含一些额外的成员用来对image进行读写。

An image view is quite literally a view into an image. It describes how to access the image and which part of the image to access, for example if it should be treated as a 2D texture depth texture without any mipmapping levels.

也就是说,ImageView帮你实现了访问Image像素之类的方法。

下文为每个Image都给创建一个ImageView。

VkImageViewCreateInfo info = {};
info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
info.viewType = VK_IMAGE_VIEW_TYPE_2D;
info.format = wd->SurfaceFormat.format;
info.components.r = VK_COMPONENT_SWIZZLE_R;
info.components.g = VK_COMPONENT_SWIZZLE_G;
info.components.b = VK_COMPONENT_SWIZZLE_B;
info.components.a = VK_COMPONENT_SWIZZLE_A;
VkImageSubresourceRange image_range = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 };
info.subresourceRange = image_range;
for (uint32_t i = 0; i < wd->ImageCount; i++)
{
    ImGui_ImplVulkanH_Frame* fd = &wd->Frames[i];
    info.image = fd->Backbuffer;
    err = vkCreateImageView(device, &info, allocator, &fd->BackbufferView);
    check_vk_result(err);
}

创建FrameBuffer

Frame buffer(帧缓冲区)封装了 color buffer image和depth buffer image。其中color buffer image为从swap chain获取的image,frame buffer的创建个数需要跟swap chain的image的数量对应,比如,双缓冲的swap chain需要对应建立2个frame buffer。

Frame buffer还负责把render pass的attachment跟ImageView关联起来。[引用]v2-e3953472599247115e1d7635f8561786_720w

VkImageView attachment[1];
VkFramebufferCreateInfo info = {};
info.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
info.renderPass = wd->RenderPass;
info.attachmentCount = 1;
info.pAttachments = attachment;
info.width = wd->Width;
info.height = wd->Height;
info.layers = 1;
// 每个ImageView需要一个FrameBuffer
for (uint32_t i = 0; i < wd->ImageCount; i++)
{
    ImGui_ImplVulkanH_Frame* fd = &wd->Frames[i];
    attachment[0] = fd->BackbufferView;
    err = vkCreateFramebuffer(device, &info, allocator, &fd->Framebuffer);
    check_vk_result(err);
}

创建Command Pool和CommandBuffers及其他

Command Buffer是储存GPU绘制命令的Buffer。同时还创建每个Command Buffer的信号量,用来完成每个渲染批次之间的同步。和所有的Pool一样,Command Buffer的内存需要用Pool来分配。

可以看到Fence和Semaphore都会在vkQueueSubmit时作为参数传入,不同之处是,Fence用于阻塞CPU直到Queue中的命令执行结束(GPU、CPU之间的同步),而Semaphore用于不同的命令提交之间的同步(GPU、GPU之间的同步)。[引用]

VkResult err;
// 每个Image都要一个Command Buffer,不知道为啥
for (uint32_t i = 0; i < wd->ImageCount; i++)
{
    ImGui_ImplVulkanH_Frame* fd = &wd->Frames[i];
    ImGui_ImplVulkanH_FrameSemaphores* fsd = &wd->FrameSemaphores[i];
    {
        // 创建Command Pool
        VkCommandPoolCreateInfo info = {};
        info.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
		// Command Buffer对象之间相互独立,不会被一起重置
        info.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
        info.queueFamilyIndex = queue_family;
        err = vkCreateCommandPool(device, &info, allocator, &fd->CommandPool);
        check_vk_result(err);
    }
    {
        // 创建Command Buffer
        VkCommandBufferAllocateInfo info = {};
        info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
        info.commandPool = fd->CommandPool;
        // Primary Command Buffer不能被其他Command Buffer调用
        info.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
        info.commandBufferCount = 1;
        err = vkAllocateCommandBuffers(device, &info, &fd->CommandBuffer);
        check_vk_result(err);
    }
    {
        // 创建fence
        VkFenceCreateInfo info = {};
        info.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
        info.flags = VK_FENCE_CREATE_SIGNALED_BIT;
        err = vkCreateFence(device, &info, allocator, &fd->Fence);
        check_vk_result(err);
    }
    {
        // 创建获取和完成的信号量(?为啥是两个)
        VkSemaphoreCreateInfo info = {};
        info.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
        err = vkCreateSemaphore(device, &info, allocator, &fsd->ImageAcquiredSemaphore);
        check_vk_result(err);
        err = vkCreateSemaphore(device, &info, allocator, &fsd->RenderCompleteSemaphore);
        check_vk_result(err);
    }
}

创建管线

一个Sampler包含了LOD(mip),Filter(插值),adressMode(平铺方式(即uv范围之外如何进行采样))

VkSamplerCreateInfo info = {};
info.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
info.magFilter = VK_FILTER_LINEAR;
info.minFilter = VK_FILTER_LINEAR;
info.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
info.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT;
info.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT;
info.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT;
info.minLod = -1000;
info.maxLod = 1000;
info.maxAnisotropy = 1.0f;
err = vkCreateSampler(v->Device, &info, v->Allocator, &bd->FontSampler);

Set DescriptorSetLayout

前文说到,一个descriptor大概算是Shader里的一个变量,大概需要将descriptor绑定到真实的GPU变量上。在以此创建LayoutBingding之后再创建Layout。

VkSampler sampler[1] = {bd->FontSampler};
VkDescriptorSetLayoutBinding binding[1] = {};
binding[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
binding[0].descriptorCount = 1;
binding[0].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
binding[0].pImmutableSamplers = sampler; 
VkDescriptorSetLayoutCreateInfo info = {};
info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
info.bindingCount = 1;
info.pBindings = binding;
err = vkCreateDescriptorSetLayout(v->Device, &info, v->Allocator, &bd->DescriptorSetLayout);
check_vk_result(err);

如果要创建例如MVP之类的变量,LayoutBinding可以使用下列代码:

VkDescriptorSetLayoutBinding layout_binding = {};
// bingding表示是DescriptorSet中的第几个
layout_binding.binding = 0;
layout_binding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
// count表示该DescriptorSet里有多少个Descriptor
layout_binding.descriptorCount = 1;
layout_binding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
layout_binding.pImmutableSamplers = NULL;

VkDescriptorSetLayoutCreateInfo descriptor_layout = {};
descriptor_layout.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
descriptor_layout.pNext = NULL;
descriptor_layout.bindingCount = 1;
descriptor_layout.pBindings = layout_bindings;

info.desc_layout.resize(NUM_DESCRIPTOR_SETS);
res = vkCreateDescriptorSetLayout(info.device, &descriptor_layout, NULL, info.desc_layout.data());
assert(res == VK_SUCCESS);

Set PipelineLayout

实际上在创建SetLayout之后应该创建Set实例的,但是imgui是先把这些layout信息创建好之后调用ImGui_ImplVulkan_CreatePipeline一起创建pipeline。

// Constants: we are using 'vec2 offset' and 'vec2 scale' instead of a full 3d projection matrix
// 和OpenGL一样的,需要手动指定变量的偏移和大小
VkPushConstantRange push_constants[1] = {};
push_constants[0].stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
// 由于是第一个变量 因此offset == 0
push_constants[0].offset = sizeof(float) * 0;
push_constants[0].size = sizeof(float) * 4;
VkDescriptorSetLayout set_layout[1] = { bd->DescriptorSetLayout };
VkPipelineLayoutCreateInfo layout_info = {};
layout_info.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
layout_info.setLayoutCount = 1;
// 绑定数个DescriptorSetLayout
layout_info.pSetLayouts = set_layout;
layout_info.pushConstantRangeCount = 1;
layout_info.pPushConstantRanges = push_constants;
err = vkCreatePipelineLayout(v->Device, &layout_info, v->Allocator, &bd->PipelineLayout);
check_vk_result(err);

创建Shader

Shader Module

创建Vertex Shader和Fragment Shader。

VkShaderModuleCreateInfo vert_info = {};
vert_info.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
vert_info.codeSize = sizeof(__glsl_shader_vert_spv);
vert_info.pCode = (uint32_t*)__glsl_shader_vert_spv;
VkResult err = vkCreateShaderModule(device, &vert_info, allocator, &bd->ShaderModuleVert);
check_vk_result(err);

VkShaderModuleCreateInfo frag_info = {};
frag_info.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
frag_info.codeSize = sizeof(__glsl_shader_frag_spv);
frag_info.pCode = (uint32_t*)__glsl_shader_frag_spv;
VkResult err = vkCreateShaderModule(device, &frag_info, allocator, &bd->ShaderModuleFrag);
check_vk_result(err);

上面的codeSize居然是编译过后的spv中间代码的长度——也就是说__glsl_shader_frag_spv是纯纯spv中间码,何其的hardcode!。

下面是编译之前的Vertex和Fragment Sahder。

#version 450 core
layout(location = 0) in vec2 aPos;
layout(location = 1) in vec2 aUV;
layout(location = 2) in vec4 aColor;
layout(push_constant) uniform uPushConstant { vec2 uScale; vec2 uTranslate; } pc;

out gl_PerVertex { vec4 gl_Position; };
layout(location = 0) out struct { vec4 Color; vec2 UV; } Out;

void main()
{
    Out.Color = aColor;
    Out.UV = aUV;
    gl_Position = vec4(aPos * pc.uScale + pc.uTranslate, 0, 1);
}
#version 450 core
layout(location = 0) out vec4 fColor;
layout(set=0, binding=0) uniform sampler2D sTexture;
layout(location = 0) in struct { vec4 Color; vec2 UV; } In;
void main()
{
    fColor = In.Color * texture(sTexture, In.UV.st);
}

Shader Stage

每个Stage对应一个可编程(大概)的着色器。看了下Stage类型蛮多的,真正能用与渲染的大概只有tessellation和geometry,compute大概可以辅助计算?

VkPipelineShaderStageCreateInfo stage[2] = {};
stage[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
stage[0].stage = VK_SHADER_STAGE_VERTEX_BIT;
stage[0].module = bd->ShaderModuleVert;
stage[0].pName = "main";

stage[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
stage[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT;
stage[1].module = bd->ShaderModuleFrag;
stage[1].pName = "main";

创建Vertex Input Description

必须注意,description不是descriptor!!!!description只是用来描述Vertex Input的结构。Vertex Buffer在运行时进行绑定。

Binding Point对Vertex Buffer的数据组织是无知的,它只关心数据块的大小和以什么样的速度(每Vertex/每Instance)更换对应的数据块,这些东西由Binding Description来解释。[引用]

Vertex Shader 对Vertex Buffer Binding是无知的,它所关心的只有Location。而Attribute Description就负责将一个Stride大小的数据块解释为Vertex Shader所关心的Location。

// Vertex Input Binding Description
// 用来描述Vertex Input的大小和输入速度
VkVertexInputBindingDescription binding_desc[1] = {};
// 下面ImDrawVert其实就是包含了pos、uv、color三个属性的结构体。
binding_desc[0].stride = sizeof(ImDrawVert);
binding_desc[0].inputRate = VK_VERTEX_INPUT_RATE_VERTEX;

// Vertex Input Attribute Description
// 用来描述所有Vertex Input的格式(我觉得这里用layout来描述比较合适)
// 第一个属性是vector2的position,location为0,offset为0
VkVertexInputAttributeDescription attribute_desc[3] = {};
attribute_desc[0].location = 0;
attribute_desc[0].binding = binding_desc[0].binding;
attribute_desc[0].format = VK_FORMAT_R32G32_SFLOAT;
attribute_desc[0].offset = IM_OFFSETOF(ImDrawVert, pos);
// 第二个属性是vector2的uv,location为1,offset为8
attribute_desc[1].location = 1;
attribute_desc[1].binding = binding_desc[0].binding;
attribute_desc[1].format = VK_FORMAT_R32G32_SFLOAT;
attribute_desc[1].offset = IM_OFFSETOF(ImDrawVert, uv);
// 第三个属性为color的col,location为2,offset为16
attribute_desc[2].location = 2;
attribute_desc[2].binding = binding_desc[0].binding;
attribute_desc[2].format = VK_FORMAT_R8G8B8A8_UNORM;
attribute_desc[2].offset = IM_OFFSETOF(ImDrawVert, col);

v2-97dd2ffbd0fff80f1f8dfc734f6ea073_r

创建一大堆Create Info

创建一大堆CreateInfo为创建pipeline做准备。

Vertex Input

VkPipelineVertexInputStateCreateInfo vertex_info = {};
vertex_info.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
vertex_info.vertexBindingDescriptionCount = 1;
vertex_info.pVertexBindingDescriptions = binding_desc;
vertex_info.vertexAttributeDescriptionCount = 3;
// 用到了上文的Vertex Input Description
vertex_info.pVertexAttributeDescriptions = attribute_desc;

Assembly

TRIANGLE_LIST就是每个三角形都有三个单独的顶点,蛮浪费空间的。

VkPipelineInputAssemblyStateCreateInfo ia_info = {};
ia_info.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
ia_info.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;

Viewport

视窗描述了输出将被渲染到的帧缓冲区域。

VkPipelineViewportStateCreateInfo viewport_info = {};
viewport_info.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
viewport_info.viewportCount = 1;
// scissor是对视口进行裁剪
viewport_info.scissorCount = 1;

rasterization

光栅化是将三角形填充成片元的过程。

这里的polygon mode是指从三角面生成片元的方式。让人没想到的是,原来vk的光栅化的polygon mode不只是只有三角形填充,还有以下模式:

  • VK_POLYGON_MODE_FILL。填充

  • VK_POLYGON_MODE_LINE。只光栅化三角形的边,即wireframe。此模式可以设置linewidth,而最大linewidth取决于硬件,由此可见这是一个硬件处理的渲染方式(废话)。

  • VK_POLYGON_MODE_POINT。渲染顶点。

  • VK_POLYGON_MODE_FILL_RECTANGLE_NV specifies that polygons are rendered using polygon rasterization rules, modified to consider a sample within the primitive if the sample location is inside the axis-aligned bounding box of the triangle after projection. Note that the barycentric weights used in attribute interpolation can extend outside the range [0,1] when these primitives are shaded. Special treatment is given to a sample position on the boundary edge of the bounding box. In such a case, if two rectangles lie on either side of a common edge (with identical endpoints) on which a sample position lies, then exactly one of the triangles must produce a fragment that covers that sample during rasterization.

    Polygons rendered in VK_POLYGON_MODE_FILL_RECTANGLE_NV mode may be clipped by the frustum or by user clip planes. If clipping is applied, the triangle is culled rather than clipped.

    Area calculation and facingness are determined for VK_POLYGON_MODE_FILL_RECTANGLE_NV mode using the triangle’s vertices. 好像是说此模式只会将AABB包围盒里的采样点光栅化为片元(难道说普通的fill会把包围盒外边的也渲染?) 看了以上文字才发现自己图形学知识浅薄,之前一直知道四边形使用双线性双三次插值,以为三角形也可以类推,没想到三角形插值使用的是三角形的重心坐标系进行插值,不过原理也极其简单,把顶点A当作起点,AB,AC作为基向量就可以表示三角形内外所有位置的坐标,进而进行插值了。[文章]

VkPipelineRasterizationStateCreateInfo raster_info = {};
raster_info.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
raster_info.polygonMode = VK_POLYGON_MODE_FILL;
// 可以进行正面或背面剔除(由此可见 剔除是在rasterization进行的,shaderlab只是进行了一些配置)
raster_info.cullMode = VK_CULL_MODE_NONE;
// frontFace表示 识别为正面的三角形的点的顺序 这里表示当组成三角面的三个点顺序是顺时针时为正面
raster_info.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
raster_info.lineWidth = 1.0f;

Multisample

MSAA

VkPipelineMultisampleStateCreateInfo ms_info = {};
ms_info.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
ms_info.rasterizationSamples = (MSAASamples != 0) ? MSAASamples : VK_SAMPLE_COUNT_1_BIT;

ColorBlendAttachment

在fragment shader渲染完之后的像素如何和帧缓冲内的像素进行混合。

VkPipelineColorBlendAttachmentState color_attachment[1] = {};
color_attachment[0].blendEnable = VK_TRUE;
color_attachment[0].srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
color_attachment[0].dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
color_attachment[0].colorBlendOp = VK_BLEND_OP_ADD;
color_attachment[0].srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
color_attachment[0].dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
color_attachment[0].alphaBlendOp = VK_BLEND_OP_ADD;
// writemask指明要输出哪些通道
color_attachment[0].colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;

Stencil

VkPipelineDepthStencilStateCreateInfo depth_info = {};
depth_info.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;

ColorBlend

把上面的ColorBlendAttachment加进去。

VkPipelineColorBlendStateCreateInfo blend_info = {};
blend_info.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
blend_info.attachmentCount = 1;
blend_info.pAttachments = color_attachment;

Dynamic

有些属性是可以不修改管线,仅仅改变参数就可以修改的。下列代码就将ViewPort的大小,Scissor作为可动态修改的。

以下枚举内的都是可动态修改的。

typedef enum VkDynamicState {
    VK_DYNAMIC_STATE_VIEWPORT = 0,
    VK_DYNAMIC_STATE_SCISSOR = 1,
    VK_DYNAMIC_STATE_LINE_WIDTH = 2,
    VK_DYNAMIC_STATE_DEPTH_BIAS = 3,
    VK_DYNAMIC_STATE_BLEND_CONSTANTS = 4,
    VK_DYNAMIC_STATE_DEPTH_BOUNDS = 5,
    VK_DYNAMIC_STATE_STENCIL_COMPARE_MASK = 6,
    VK_DYNAMIC_STATE_STENCIL_WRITE_MASK = 7,
    VK_DYNAMIC_STATE_STENCIL_REFERENCE = 8,
    VK_DYNAMIC_STATE_VIEWPORT_W_SCALING_NV = 1000087000,
    VK_DYNAMIC_STATE_DISCARD_RECTANGLE_EXT = 1000099000,
    VK_DYNAMIC_STATE_SAMPLE_LOCATIONS_EXT = 1000143000,
    VK_DYNAMIC_STATE_RAY_TRACING_PIPELINE_STACK_SIZE_KHR = 1000347000,
    VK_DYNAMIC_STATE_VIEWPORT_SHADING_RATE_PALETTE_NV = 1000164004,
    VK_DYNAMIC_STATE_VIEWPORT_COARSE_SAMPLE_ORDER_NV = 1000164006,
    VK_DYNAMIC_STATE_EXCLUSIVE_SCISSOR_NV = 1000205001,
    VK_DYNAMIC_STATE_FRAGMENT_SHADING_RATE_KHR = 1000226000,
    VK_DYNAMIC_STATE_LINE_STIPPLE_EXT = 1000259000,
    VK_DYNAMIC_STATE_CULL_MODE_EXT = 1000267000,
    VK_DYNAMIC_STATE_FRONT_FACE_EXT = 1000267001,
    VK_DYNAMIC_STATE_PRIMITIVE_TOPOLOGY_EXT = 1000267002,
    VK_DYNAMIC_STATE_VIEWPORT_WITH_COUNT_EXT = 1000267003,
    VK_DYNAMIC_STATE_SCISSOR_WITH_COUNT_EXT = 1000267004,
    VK_DYNAMIC_STATE_VERTEX_INPUT_BINDING_STRIDE_EXT = 1000267005,
    VK_DYNAMIC_STATE_DEPTH_TEST_ENABLE_EXT = 1000267006,
    VK_DYNAMIC_STATE_DEPTH_WRITE_ENABLE_EXT = 1000267007,
    VK_DYNAMIC_STATE_DEPTH_COMPARE_OP_EXT = 1000267008,
    VK_DYNAMIC_STATE_DEPTH_BOUNDS_TEST_ENABLE_EXT = 1000267009,
    VK_DYNAMIC_STATE_STENCIL_TEST_ENABLE_EXT = 1000267010,
    VK_DYNAMIC_STATE_STENCIL_OP_EXT = 1000267011,
    VK_DYNAMIC_STATE_VERTEX_INPUT_EXT = 1000352000,
    VK_DYNAMIC_STATE_PATCH_CONTROL_POINTS_EXT = 1000377000,
    VK_DYNAMIC_STATE_RASTERIZER_DISCARD_ENABLE_EXT = 1000377001,
    VK_DYNAMIC_STATE_DEPTH_BIAS_ENABLE_EXT = 1000377002,
    VK_DYNAMIC_STATE_LOGIC_OP_EXT = 1000377003,
    VK_DYNAMIC_STATE_PRIMITIVE_RESTART_ENABLE_EXT = 1000377004,
    VK_DYNAMIC_STATE_COLOR_WRITE_ENABLE_EXT = 1000381000,
    VK_DYNAMIC_STATE_MAX_ENUM = 0x7FFFFFFF
} VkDynamicState;

下为imgui中使用的代码。

VkDynamicState dynamic_states[2] = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR };
VkPipelineDynamicStateCreateInfo dynamic_state = {};
dynamic_state.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
dynamic_state.dynamicStateCount = (uint32_t)IM_ARRAYSIZE(dynamic_states);
dynamic_state.pDynamicStates = dynamic_states;

创建管线

填充各种info然后创建pipeline。

VkGraphicsPipelineCreateInfo info = {};
info.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
info.flags = bd->PipelineCreateFlags;
info.stageCount = 2;
info.pStages = stage;
info.pVertexInputState = &vertex_info;
info.pInputAssemblyState = &ia_info;
info.pViewportState = &viewport_info;
info.pRasterizationState = &raster_info;
info.pMultisampleState = &ms_info;
info.pDepthStencilState = &depth_info;
info.pColorBlendState = &blend_info;
info.pDynamicState = &dynamic_state;
info.layout = bd->PipelineLayout;
info.renderPass = renderPass;
info.subpass = subpass;
VkResult err = vkCreateGraphicsPipelines(device, pipelineCache, 1, &info, allocator, pipeline);

更新字体(?)

// Use any command queue
// 这里的FrameIndex是窗口渲染管线的RenderPass的第二个FrameBuffer
VkCommandPool command_pool = wd->Frames[wd->FrameIndex].CommandPool;
VkCommandBuffer command_buffer = wd->Frames[wd->FrameIndex].CommandBuffer;

err = vkResetCommandPool(g_Device, command_pool, 0);
check_vk_result(err);
VkCommandBufferBeginInfo begin_info = {};
begin_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
begin_info.flags |= VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
// 从这里开始设置渲染命令
err = vkBeginCommandBuffer(command_buffer, &begin_info);
check_vk_result(err);

ImGui_ImplVulkan_CreateFontsTexture(command_buffer);

VkSubmitInfo end_info = {};
end_info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
end_info.commandBufferCount = 1;
end_info.pCommandBuffers = &command_buffer;
err = vkEndCommandBuffer(command_buffer);
check_vk_result(err);
// 提交到队列
err = vkQueueSubmit(g_Queue, 1, &end_info, VK_NULL_HANDLE);
check_vk_result(err);

err = vkDeviceWaitIdle(g_Device);
check_vk_result(err);
ImGui_ImplVulkan_DestroyFontUploadObjects();

以上是整体的结构,其中imgui的函数ImGui_ImplVulkan_CreateFontsTexture还蛮复杂。

关于ImGui如何获取字体信息,参看文字渲染.md

一些概念

Image | ImageView | FrameBuffer | RenderTarget | Attachment

图片信息真正的储存位置是VkMemory,VkDeviceMemory,以字节形式存储。 而Image首先包含了一些色彩格式之类的元信息,然后提供了使用Texel进行访问RGBA的方法。 ImageView就是提供了以不同view来读取Image信息的方法。例如提供了reinterpret方法,可以将原本的RGBA给reinterpret成FT(F=RG, T=BA)、读取Image指定mip的数据、读取Image的指定array(没大懂)。

Attachment分为Color和Depth,但Attachment正如其名,只是一个附件,只储存了一些元信息。

FrameBuffer将ImageView和对应的Attachment绑定到一起。

RenderTarget是一个Subpass渲染的结果,但是好像在Vulkan api里并不存在RT的概念,而是通过Attachments来实现的,一个Subpass会接受来自上一个Subpass的Attachments作为输入,然后又会输出计算后的Attachments。

FrameBuffers

K0NRD